<?php
/**
 * Signalsy Platform Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@wheemplay.com so we can send you a copy immediately.
 *
 * @category   Signalsy
 * @package    Signalsy Router
 * @copyright  Copyright (c) 2009 AGPsource Team
 * @license    http://signalsy.com/license/ New BSD License
 */

 
 /* including Exceptions */
 include_once ( '/inc/lib/Signalsy/Exceptions.php' );
 

/**
 * Router is main class to provide signal's routing and preparing all data
 *
 * @category   Signalsy
 * @package    Signalsy Router
 * @copyright  Copyright (c) 2009 AGPsource Team
 * @license    http://signalsy.com/license/     New BSD License
 */
 class Signalsy_Router
 {
 	/**
 	 * @var Array config section for this component
 	 */
 	private $config = null;
 	
 	/**
 	 * @var Object object instance for singlton
 	 * @access public
 	 * @static
 	 */
	static private $instance = null;
	
	/**
	 * @var Object Zend_Cache object if used cache mechanism
	 */
	protected $_cache = null;	
	
	/**
	 * @var Array Currently allowed to use signal's namespace
	 * Note: all signal must be in namespace, signal without namespace are invalide and generate exceptions
	 */
	protected $_namespaces = Array('sys', 'error', 'core', 'service', 'test', 'admin');
	
 	/**
 	 * @var Array current routing table, ordered by emmit position
 	 */
	protected $_current_route_table = Array();
	
	/**
	 * @var Array Required signals, emmited before all associated with request URL
	 */
	protected $_pre_dispatch = Array();
	
	/**
	 * @var Array Required signals, emmited after all associated with request URL
	 */
	protected $_post_dispatch = Array();
	
	/**
	 * @var Array Signals table, if empty or invalid (or unknown) URL and no signal table associated
	 */
	protected $_default_empty_url_signal = Array('sys_error_404');
	
	/**
	 * @var Array If signals can be emmited more then one at URL processing, must be in this array,
	 * otherwise, it's be skipped (if be callable above) 
	 */
	protected $_multiple_signals = Array('sys_exception', 'sys_session');
	
	
	/**
	 *@var Array Connecting table with all signals connector, from cache
	 */
	protected $__connectings_table = Array();
	protected $__routings_table = Array();
	
	/**
	 * @var Array Processed signals
	 */
	protected $_emmited = Array();
	protected $_emmited_results = Array();
	
	/**
	 * @var Array Full param array to all signals calling
	 */
	private $param = Array();
	
	/**
	 * @var int maximum slots priority per signal
	 */
	protected $maxSlotsPerSignal = 65550;
	
	
	/**
	 * *******************************************************************************************
	 */
	
	/**
	 * @access private
	 * @param Array config array
	 * @return void
	 */ 
	private function __construct($config = null)
	{
		if (empty($config))
		{
			$this->config = parse_ini_file('./inc/config/game_config.ini', true);	
		}
		else
			$this->config = $config;
		
		// Construct or getting from cache routings and connectings tables
		$this->_constructRoutesTable();
				
		//Init required params
		$this->createParam();
				
		//add config to params		
		$this->setParamEntity('config', $this->config);
		
	}
	
	private function __clone(){
		throw new Signalsy_Exception('Do not clone router object!');
	}
	
	/**
	 * @access public
	 * @static
	 * @return Object  Signalsy_Router instance
	 */
	public static function getInstance($config = null)
	{
		if (isset(self::$instance))
		return self::$instance;
		else
			{
				self::$instance = new Signalsy_Router($config);
				return self::$instance;
			}
	}
	
	
	/**
	 * Construct or gets from cache routing table
	 * In this, we use simple file cache or APC/XCache system (without Zend Cache)
	 * @access private
	 * @return Boolean 
	 */
	private function _constructRoutesTable()
	{
		if (($this->config['Signals']['cache_url_routing_table'] == true) && (empty($this->_cache)))
		{
			$this->_prepareCache();
		}
		
		//trying to load table from cache
		if (!$result = $this->_cache->load('__routings_table'))
		{
			//construct manually
			$this->__routings_table = array_merge(Framework_connectManager::exportRouting(),
											      Test_connectManager::exportRouting(),
												  Admin_connectManager::exportRouting(),
												  App_connectManager::exportRouting());
						
			//add to cache
			$this->_cache->save($this->__routings_table, '__routings_table');
		}
		else
			{
				$this->__routings_table = $result;
			}
		
		//trying to load connecting table
		if (!$result = $this->_cache->load('__connectings_table'))
		{
			//construct manually
			$this->__connectings_table = array_merge(Framework_connectManager::exportSignals(),
			 									     Admin_connectManager::exportSignals(),
													 App_connectManager::exportSignals(),
													 Test_connectManager::exportSignals());
			
			//add to cache
			$this->_cache->save($this->__connectings_table, '__connectings_table');
		}
		else
			{
				$this->__connectings_table = $result;
			}
		
		
		return true;		
	}
	
	
	/**
	 * Utilite function, prepare cache if using routes cache
	 * @return Object Zend_Cache
	 */
	private function _prepareCache()
	{
		if (empty($this->config['Signals']['cache_backend_name']))
		{
			$backend_cache = 'File';
			
			$backend_cache_options = Array(
				'cache_dir' => $this->config['Cache']['cache_path'],
				'file_locking' => false,
				'read_control' => true,
				'read_control_type' => 'crc32'				
			);
			
			
		}
		else
			{
				$backend_cache = $this->config['Signals']['cache_backend_name'];
				
				//for XCache
				$backend_cache_options = Array(
					'slow_backend' => 'File',
					'fast_backend' => 'Xcache',   //'Zend_Cache_Backend_' . $backend_cache,
					'slow_backend_options' => Array(
													'cache_dir' => $this->config['Cache']['cache_path'],
													'file_locking' => false,
													'read_control' => true,
													'read_control_type' => 'crc32'				
												),
					'fast_backend_options' => Array(
													'user' => '',
													'password' => ''
												),
					'auto_refresh_fast_cache' => true
				);
				
			}
		
		
		$this->_cache = Zend_Cache::factory('Core', $backend_cache, 
			array(
				'caching' => true,
				'cache_id_prefix' => 'ssy_',
				'logging' => false,
				'write_сontrol' => false,
				'automatic_serialization' => true,
				'automatic_cleaning_factor' => 0,
				'ignore_user_abort' => true
			), $backend_cache_options);	
			
		
		//this is default cache to all system, adding them to registry
		Zend_Registry::getInstance()->set('cache', $this->_cache);			
	}
	
	
	/**
	 * checking signal namespace
	 * @access private
	 * @param string signal to checking
	 * @return bool
	 */
	private function _checkSignalNamespace($signal = null)
	{
		if (empty($signal)) return false;
		else
			{
				$_ns = explode('_', $signal);
				
				if (!empty($_ns[0]))
				{
					if (in_array($_ns[0], $this->_namespaces)) return true;
					else
						return false;
				} 
				else
					return false;
			}
	}
	
	
	/**
	 * Create required param array
	 * @return void
	 */
	private function createParam()
	{
		$this->param = Array();
	}
	
	/**
	 * устанавливает параметр
	 * @return bool
	 * @param object $param_name[optional]
	 * @param object $param_value[optional] - либо значение, либо массив имя-значение
	 * @param boolean $replace - если true, переменная перезаписывается, если false, добавляется в массив
	 */
	public function setParamEntity($param_name = null, $param_value = null, $replace = true)
	{
		if (!empty($param_name))
		{
			//!TODO: use $replace param
			$this->param[$param_name] = $param_value;
			return true;
		}
		else
			return false;
	}
		

	/**
	 * Preparing HTTP params
	 */
	private function prepareHTTPRequest()
	{
		//exclude flash amf query: Content-type: application/x-amf
		
		if ((!isset($_SERVER['CONTENT_TYPE'])) || ((isset($_SERVER['CONTENT_TYPE'])) && ($_SERVER['CONTENT_TYPE'] != 'application/x-amf')))
		{		
		
			// из запроса убираются все параметры, которые начинаются с символа _, например, _dc
			$tmp = $_REQUEST;
			$httprequest = Array();
			
			foreach ($tmp as $name=>$item)
			{
				$name = substr(strtolower(trim($name)), 0, 1024); //максимальная длина имени переменной 1024 символа
				$item = substr(trim($item), 0, 4096); //максимальная длина значения, передаваемого - 4 Кб
				
				if (strpos($name, '_') === 0)    continue;
				else
				{
					$httprequest[$name] = $item;
				}
			}
			
			if (!empty($httprequest))
			{
				//добавить в параметр
				$this->setParamEntity('httprequest', $httprequest);
			}
			
		}		
		else
			$this->setParamEntity('httprequest', null);
	}



	/**
	 * Preparing URL to routing
	 * @param string URL
	 * @return string
	 */
	public function prepareURL($url = null)
	{
		if (empty($url)) return '';
		else
			{
				//!NOTE: max URL length is 4096 symbols
				$url = strtolower(substr($url, 0, 4096));
				
				if (strripos($url, '/') === (strlen($url)-1))
				{
					$url = substr($url, 0, strlen($url)-1);
				}
				
				if (!empty($url)) return $url;
				else
					return '';				
			}
	}
	
	
	/**
	 * Create full table with signals and connectings for prepared URL
	 * @param string URL
	 */
	private function prepareFullRoutingTable($url = null)
	{
		if (empty($url)) $url = ''; //default URL is '' or '/'
		
		//if configured, check cache before manually construct
		$_url_hash = md5($url);
		
		if (($this->config['Signals']['cache_url_routing_table'] == false) || 
		    (($this->config['Signals']['cache_url_routing_table'] == true) && (!$result = $this->_cache->load($_url_hash))))
		{
			/* nothing in cache, manually construct
			 * !TODO: add regext to find url, starting with $url and maximize length
			 *
			 */
			
			
			
			
			
			
			
			if (array_key_exists($url, $this->__routings_table))
			{
				//full signals table
				$_signals = array_merge($this->_pre_dispatch, 
				                        $this->__routings_table[$url],
				                        $this->_post_dispatch);
				                        
				foreach ($_signals as $sign)
				{
					if (array_key_exists($sign, $this->__connectings_table))
					{
						$tmp = $this->__connectings_table[$sign];
						
						//processing meta-signal
						if (!isset($tmp['meta']))
						{
							ksort($tmp, SORT_NUMERIC); // sorting by proirity
						
							//added
							$this->_current_route_table[] = array('signal' => $sign, 'slot' => $tmp);
						}
						else
							{
								$_mtmp = $tmp['meta']; 
								
								if ((!empty($_mtmp)) && (is_array($_mtmp)))
								{
									foreach ($_mtmp as $_msign)
									{
										if (isset($this->__connectings_table[$_msign]))
										{
											$this->_current_route_table[] = array('signal' => $_msign, 'slot' => $this->__connectings_table[$_msign]);
										}
									}
								}
								
							}
						
					}
				}
				
				//added to cache
				if ($this->config['Signals']['cache_url_routing_table'] == true)
				{
					$this->_cache->save($this->_current_route_table, $_url_hash);
				}
				
			}
			else
				throw new Signalsy_Unknown_Url_Exception('Unknown url: ' . $url);
		}
		else
			{
				$this->_current_route_table = $result;

			}
			
			
		if ($this->config['Logging']['use_firebug_logging'] == true)
		{
			Zend_Registry::getInstance()->get('ajax_log')->log($this->_current_route_table, Zend_Log::INFO);	
		}
		
		return true;			
	}
	
	
	/**
	 * Main function - dispatch URL and all signals'
	 * @param string URL
	 */
	public function dispatchURL($url = null)
	{
		$url = $this->prepareURL($url);
		
		if (!empty($url))
		{
			$this->setParamEntity('url', $url);
		}
		
	    //надо собрать в параметры все необходимые переменные
		$this->prepareHTTPRequest();
		
		//building route table
		$this->prepareFullRoutingTable($url);
		
		//processing them!
		try
		{
			foreach ($this->_current_route_table as $cur_emmit)
			{
				$this->setParamEntity('now_signal', $cur_emmit['signal']);
			
				//emiting signal
				// check, if signal be emmited or allow multiple emmit
				if (($this->isEmitted($cur_emmit['signal']) == true) && (!in_array($this->_multiple_signals)))
				{
					//go to next signal
					continue;
				}
				else
					{
						foreach ($cur_emmit['slot'] as $prior => $slot)
						{
							$result = null;
							if (is_callable($slot))
							{
								//try to call
								$result = call_user_func($slot, $this->param, null);
								
								$this->_emmited_results[] = array('signal' => $cur_emmit['signal'], 'result' => $result);
								$this->_emmited[] = $cur_emmit['signal'];
							}
							else
								throw new Signalsy_Unknown_Connect_Exception('Connect not callable');
						}
					}
			}
		}
		catch (Signalsy_Unknown_Signal_Namespace_Exception $e)
		{
			Zend_Registry::getInstance()->get('ajax_log')->log($e, Zend_Log::ERR);
		}
		catch (Signalsy_Unknown_Connect_Exception $e)
		{
			Zend_Registry::getInstance()->get('ajax_log')->log($e, Zend_Log::ERR);
		}
		catch (Signalsy_Unknown_Signal_Exception $e)
		{
			Zend_Registry::getInstance()->get('ajax_log')->log($e, Zend_Log::ERR);
		}
		catch (Signalsy_StopRouting_Exception $e)
		{
			//if routing are stopped, emit exception signal
			$this->emit('sys_exception', null, 'Routing are stopping');
		}
	
	}
	
	
	/**
	 * Check, emmited or no this signal
	 * @param string signal
	 * @return boolean
	 */
	public function isEmitted($signal = null)
	{
		if (empty($signal)) return false;
		else
			{
				if (in_array($signal, $this->_emmited)) return true;
				else
					return false;				
			}
	}
	
	/**
	 * Manually emmit signal with optionally param
	 * special thanks Appocaliptica One for this method :)	 * 
	 * 
	 * @access public
	 * @param string|array signal
	 * @param mixed optional arguments
	 * @return mixed
	 */
	public function emmit($_signal = null, $options = null)
	{
		if (empty($_signal)) return false;
		else
			{
				// may one or array of signals calling at one time
				$_signals = Array();
				
				if (is_array($_signal)) array_merge($_signals, $_signal);
				else
					if(is_string($_signal)) $_signals[] = $_signal;
				
				
				foreach ($_signals as $signal)
				{				
				
				//check signal namespace
				if ($this->_checkSignalNamespace($signal))
				{
					if (($this->isEmitted($signal) == true) && (!in_array($this->_multiple_signals)))
					{
						continue;
					}
									
					//create calling stack
					if (array_key_exists($signal, $this->__connectings_table))
					{
						$call_stack = $this->__connectings_table[$signal];
						ksort($call_stack, SORT_NUMERIC);
						
						$this->setParamEntity('now_signal', $signal);
						
						foreach ($call_stack as $prior => $slot)
						{
							if (is_callable($slot))
							{
								try
								{
									//Calling with additionals param
									$result = call_user_func($slot, $this->param, $options);
									
									$this->_emmited_results[] = array('signal' => $signal, 'result' => $result);
									$this->_emmited[] = $signal;
									
									continue;
								}
								catch (Signalsy_SignalSlot_Exception $e)
								{
									Zend_Registry::getInstance()->get('ajax_log')->log($e, Zend_Log::ERR);
								}
								catch (Signalsy_StopRouting_Exception $e)
								{
									return false;
								}
							}
						}
						
					}
					else
						throw new Signalsy_Unknown_Signal_Exception('Unknown signal or missing connections for him');
				}
				else
					throw new Signalsy_Unknown_Signal_Namespace_Exception('Bad or unknown signal namespace prefix');
				
				}
					
			}
	}
	
	
	
	
	/**
	 * Manually added handler for signal
	 */
	public function addSlotHandler($signal = null, $slot = null, $prior = 1000)
	{
		if ((empty($signal)) || ((empty($slot)) || (!is_callable($slot)))) return false;
		else
			{
				//check, if signal exist
				if (!array_key_exists($signal, $this->__connectings_table)) return false;
				else
					{
						$tmp = $this->__connectings_table[$signal];
						
						if (isset($this->__connectings_table[$signal][$prior]))
						{
							//if added prior isnt not free, see next free priority (above, hight then)
							for ($prior; $prior < $this->maxSlotsPerSignal; $prior++)
							{
								if (!isset($this->__connectings_table[$signal][$prior]))
								{
									$this->__connectings_table[$signal][$prior] = $slot;
									break;
								}
							}
							
							return true;
						}
					}				
			}
	}
	
	/**
	 * Adding new signal (may be without slot)
	 * $slot = if exist's, array (priority => string function name or array)
	 */
	public function addSignal($signal = null, $slots = null, $multiEmmit = false)
	{
		if (empty($signal)) return false;
		else
			{
				if (!array_key_exists($signal, $this->__connectings_table))
				{
					//check namespace
					if ($this->_checkSignalNamespace($signal))
					{
						$this->__connectings_table[$signal] = $slots;
						
						if ($multiEmmit == true)
						{
							$this->_multiple_signals[] = $signal;
						}
					}
					else
						throw new Signalsy_Unknown_Signal_Namespace_Exception('Bad signal namespace');
				}
				else
					return false;
			}
	}
	
	
	// debug function
	static public function getDebugInfo()
	{
	
		return Array('routes' => self::getInstance()->__routings_table, 'signals' => self::getInstance()->__connectings_table);
	}
	
	
	public function outputAll()
	{
		if ($this->config['Performance']['use_ob_handler'] == true)
		{
			if (Zend_Registry::isRegistered('ajax_log'))
			{
				$_time = microtime(true);
			
				$_d = round(($_time - Zend_Registry::getInstance()->get('start_time')), 4);
				
				Zend_Registry::getInstance()->get('ajax_log')->log('Time to request processing: ' . $_d . ' sec.', Zend_Log::INFO);
				
				Zend_Registry::get('ajax_log')->log('Loaded: ' . count(get_included_files()) .' files', Zend_Log::INFO);
				
				$mem_stop = memory_get_usage();
 				Zend_Registry::get('ajax_log')->log('Memory usage: ' . round(($mem_stop - Zend_Registry::get('mem_start')/1024), 2) . ' Kb', Zend_Log::INFO);				
				
				Zend_Registry::getInstance()->get('firephp_channel')->flush();;
				Zend_Registry::getInstance()->get('firephp_response')->sendHeaders();
			}		
			
			
			if ($this->config['Performance']['use_web_optimizer_system'] == true)
			{
				if ((isset($web_optimizer)) && (is_object($web_optimizer)))
				{
					$web_optimizer->finish();
				}
			}
			else
				{
					ob_end_flush();
				}
			
		}
	}
 }